"""
This module provides methods to register your identity with the Fetch.ai services.
"""

import urllib.parse
from json import JSONDecodeError
from typing import Literal

import requests
from pydantic import BaseModel, Field, model_validator

from uagents_core.config import (
    DEFAULT_ALMANAC_API_PATH,
    DEFAULT_REQUEST_TIMEOUT,
    AgentverseConfig,
)
from uagents_core.contrib.protocols.chat import chat_protocol_spec
from uagents_core.identity import Identity
from uagents_core.logger import get_logger
from uagents_core.protocol import ProtocolSpecification, is_valid_protocol_digest
from uagents_core.registration import (
    AgentProfile,
    AgentStatusUpdate,
    AgentverseConnectRequest,
    BatchRegistrationRequest,
    ChallengeResponse,
    IdentityProof,
    RegistrationRequest,
)
from uagents_core.types import AgentEndpoint, AgentMetadata, AgentType

logger = get_logger("uagents_core.utils.registration")


class AgentverseRegistrationRequest(BaseModel):
    """
    A model containing all information for a user to register
    their pre-existing agent to Agentverse.
    """

    name: str = Field(description="Agent name in Agentverse")
    endpoint: str = Field(
        description="Endpoint where the existing agent is accessible at."
    )
    protocols: list[str] = Field(
        description="List of protocols supported by the agent."
    )
    metadata: dict[str, str | list[str] | dict[str, str]] | None = Field(
        default=None,
        description="Additional metadata about the agent (e.g. geolocation).",
    )
    type: AgentType = Field(
        default="uagent", description="Agentverse registration type"
    )
    description: str | None = Field(
        default=None,
        description="Agent short description, shown on its Agentverse profile.",
    )
    readme: str | None = Field(default=None, description="Agent skills description.")
    avatar_url: str | None = Field(
        default=None,
        description="Agent avatar url to be shown on its Agentverse profile.",
    )
    handle: str | None = Field(
        default=None,
        max_length=40,
        description="Agent's unique handle in Agentverse.",
    )
    active: bool = Field(default=True, description="Set agent as active immediatly")

    @model_validator(mode="after")
    def check_request(self) -> "AgentverseRegistrationRequest":
        # check endpoint
        result = urllib.parse.urlparse(self.endpoint)
        if not all([result.scheme, result.netloc]):
            raise ValueError(f"Invalid endpoint provided: {self.endpoint}")

        # check protocol digests
        for proto_digest in self.protocols:
            if not is_valid_protocol_digest(proto_digest):
                raise ValueError(
                    f"Invalid protocol digest provided: {proto_digest}",
                )
        return self


class RegistrationRequestCredentials(BaseModel):
    agentverse_api_key: str = Field(
        description="Agentverse API key generated by the owner of the agent"
    )
    agent_seed_phrase: str = Field(
        description="The secret seed phrase used to create the agent identity"
    )
    team: str | None = Field(
        default=None, description="The team the agent belongs to in Agentverse"
    )


class AgentverseRequestError(Exception):
    def __init__(self, *args, from_exc: Exception):
        self.from_exc = from_exc
        super().__init__(*args)


def _send_http_request_agentverse(
    request_type: Literal["get", "post", "put"],
    url: str,
    *,
    data: BaseModel | None = None,
    headers: dict[str, str] | None = None,
    timeout: int = DEFAULT_REQUEST_TIMEOUT,
) -> requests.Response:
    final_headers: dict[str, str] = {"content-type": "application/json"}
    if headers:
        final_headers.update(headers)

    send_request = {
        "get": requests.get,
        "post": requests.post,
        "put": requests.put,
    }[request_type]

    try:
        response: requests.Response = send_request(
            url=url,
            headers=final_headers,
            data=data.model_dump_json() if data else None,
            timeout=timeout,
        )
        response.raise_for_status()
    except Exception as e:
        err_msg = ""

        if isinstance(e, requests.ConnectionError):
            err_msg += f"Connection error {e.strerror}."
        elif isinstance(e, requests.Timeout):
            err_msg += "Operation timed out."
        elif isinstance(e, requests.HTTPError):
            code = e.response.status_code
            try:
                content = e.response.json()["detail"]
            except (JSONDecodeError, KeyError):
                content = e.response.content.decode()
            if code in [401, 406, 409]:
                err_msg += content
            elif code == 500:
                err_msg += "Unexpected server error."
            else:
                err_msg += f"HTTP error: {code} {content}"
        elif isinstance(e, requests.RequestException):
            err_msg += f"Unexpected request error: {e}."
        else:
            err_msg += f"Unexpected error: {e}."

        raise AgentverseRequestError(err_msg, from_exc=e) from e

    return response


def _send_post_request_agentverse(
    url: str,
    *,
    data: BaseModel | None = None,
    headers: dict[str, str] | None = None,
    timeout: int = DEFAULT_REQUEST_TIMEOUT,
) -> requests.Response:
    return _send_http_request_agentverse(
        "post", url, data=data, headers=headers, timeout=timeout
    )


def _register_in_agentverse(
    request: AgentverseConnectRequest,
    identity: Identity,
    *,
    agent_details: AgentverseRegistrationRequest,
    agentverse_config: AgentverseConfig | None = None,
    timeout: int = DEFAULT_REQUEST_TIMEOUT,
):
    """
    Register an agent in Agentverse and update its details if provided.

    Args:
        request (AgentverseConnectRequest): The request containing the agent details.
        identity (Identity): The identity of the agent.
        agent_details (AgentverseRegistrationRequest): The agent details to update.
        agentverse_config (AgentverseConfig | None): The configuration for the agentverse API
        timeout (int): The timeout for the requests
    """
    agentverse_config = agentverse_config or AgentverseConfig()
    agents_api = agentverse_config.agents_api
    identity_api = agentverse_config.identity_api

    agent_address = identity.address

    registration_metadata = {
        "agents_api": agents_api,
        "agent_address": agent_address,
        "agent_endpoint": request.endpoint or "",
        "agent_type": request.agent_type,
        "agent_name": agent_details.name,
    }

    headers = {
        "content-type": "application/json",
        "authorization": f"Bearer {request.user_token}",
    }
    if request.team:
        headers["x-team"] = request.team

    # check to see if the agent exists
    response = requests.get(
        f"{agents_api}/{agent_address}",
        headers=headers,
        timeout=timeout,
    )

    # if it doesn't then create it
    if response.status_code == 404:
        logger.debug(
            msg="Agent does not exist on agentverse; registering it...",
            extra=registration_metadata,
        )

        challenge_api = f"{identity_api}/{agent_address}/challenge"
        logger.debug(
            msg="Requesting proof-of-ownership challenge",
            extra={**registration_metadata, "challenge_api": challenge_api},
        )
        try:
            response = _send_http_request_agentverse(
                request_type="get",
                url=challenge_api,
                data=None,
                headers={"authorization": f"Bearer {request.user_token}"},
                timeout=timeout,
            )
        except AgentverseRequestError as e:
            raise AgentverseRequestError(
                f"failed to request proof-of-ownership challenge. {str(e)}",
                from_exc=e.from_exc,
            ) from e

        challenge = ChallengeResponse.model_validate_json(response.text)
        identity_proof = IdentityProof(
            address=identity.address,
            challenge=challenge.challenge,
            challenge_response=identity.sign(challenge.challenge.encode()),
        )

        response = _send_post_request_agentverse(
            url=identity_api,
            data=identity_proof,
            headers=headers,
            timeout=timeout,
        )

    # update the readme and the name of the agent to make it easier to find
    logger.debug(
        msg="Registering agent details with Agentverse",
        extra=registration_metadata,
    )

    reg_request = RegistrationRequest(
        address=agent_address,
        name=agent_details.name,
        handle=agent_details.handle,
        url=agent_details.avatar_url,
        agent_type=agent_details.type,
        profile=AgentProfile(
            description=agent_details.description or "",
            readme=agent_details.readme or "",
            avatar_url=agent_details.avatar_url or "",
        ),
        endpoints=[AgentEndpoint(url=agent_details.endpoint, weight=1)],
        protocols=agent_details.protocols,
        metadata=agent_details.metadata,
    )

    try:
        _send_post_request_agentverse(
            url=agents_api,
            headers=headers,
            data=reg_request,
            timeout=timeout,
        )
    except AgentverseRequestError as e:
        logger.warning(f"failed to register agent. {str(e)}")


def register_in_agentverse(
    request: AgentverseConnectRequest,
    identity: Identity,
    agent_details: AgentverseRegistrationRequest,
    *,
    agentverse_config: AgentverseConfig | None = None,
    timeout: int = DEFAULT_REQUEST_TIMEOUT,
) -> bool:
    """
    Register an agent in Agentverse and update its details if provided.

    Args:
        request (AgentverseConnectRequest): The request containing the agent details.
        identity (Identity): The identity of the agent.
        agent_details (AgentverseRegistrationRequest): The agent details to update.
        agentverse_config (AgentverseConfig | None): The configuration for the agentverse API
        timeout (int): The timeout for the requests
    """
    try:
        _register_in_agentverse(
            request,
            identity,
            agent_details=agent_details,
            agentverse_config=agentverse_config,
            timeout=timeout,
        )
        return True
    except AgentverseRequestError as e:
        logger.error(msg=str(e), exc_info=e.from_exc)

    return False


def _update_agent_status(active: bool, identity: Identity):
    """
    Update the agent's active/inactive status in the Almanac API.

    Args:
        active (bool): The status of the agent.
        identity (Identity): The identity of the agent.
    """
    almanac_api = AgentverseConfig().url + DEFAULT_ALMANAC_API_PATH

    status_update = AgentStatusUpdate(
        agent_identifier=identity.address, is_active=active
    )
    status_update.sign(identity)

    logger.debug(
        msg="Updating agent status in Almanac API",
        extra=status_update.model_dump(),
    )

    _send_post_request_agentverse(
        url=f"{almanac_api}/agents/{identity.address}/status",
        data=status_update,
    )


def update_agent_status(active: bool, identity: Identity) -> bool:
    """
    Update the agent's active/inactive status in the Almanac API.

    Args:
        active (bool): The status of the agent.
        identity (Identity): The identity of the agent.
    """
    try:
        _update_agent_status(active, identity)
        return True
    except AgentverseRequestError as e:
        logger.error(msg=str(e), exc_info=e.from_exc)

    return False


def register_agent(
    agent_registration: AgentverseRegistrationRequest,
    agentverse_config: AgentverseConfig,
    credentials: RegistrationRequestCredentials,
):
    identity = Identity.from_seed(credentials.agent_seed_phrase, 0)
    connect_request = AgentverseConnectRequest(
        user_token=credentials.agentverse_api_key,
        agent_type=agent_registration.type,
        endpoint=agent_registration.endpoint,
        team=credentials.team,
    )

    # register the agent to agentverse
    try:
        logger.info("registering to Agentverse...")
        _register_in_agentverse(
            connect_request,
            identity,
            agent_details=agent_registration,
            agentverse_config=agentverse_config,
        )
        logger.info("successfully registered to Agentverse.")
    except AgentverseRequestError as e:
        logger.error(f"failed to register to Agentverse. {str(e)}")
        return

    # set agent as active
    if agent_registration.active:
        try:
            logger.info("setting agent as active...")
            _update_agent_status(True, identity)
            logger.info("successfully set agent to active.")
        except AgentverseRequestError as e:
            logger.warning(f"failed to set agent as active. {str(e)}")


def register_chat_agent(
    name: str,
    endpoint: str,
    active: bool,
    credentials: RegistrationRequestCredentials,
    description: str | None = None,
    readme: str | None = None,
    avatar_url: str | None = None,
    metadata: AgentMetadata | dict[str, str | list[str] | dict[str, str]] | None = None,
    agentverse_config: AgentverseConfig | None = None,
):
    chat_protocol = [
        ProtocolSpecification.compute_digest(chat_protocol_spec.manifest())
    ]
    raw_metadata = (
        metadata.model_dump(exclude_unset=True)
        if isinstance(metadata, AgentMetadata)
        else metadata
    )
    request = AgentverseRegistrationRequest(
        name=name,
        endpoint=endpoint,
        protocols=chat_protocol,
        active=active,
        description=description,
        readme=readme,
        avatar_url=avatar_url,
        metadata=raw_metadata,
    )
    config = agentverse_config or AgentverseConfig()

    register_agent(request, config, credentials)


def register_batch_in_agentverse(
    batch_request: BatchRegistrationRequest,
    user_token: str,
    *,
    agentverse_config: AgentverseConfig | None = None,
    timeout: int = DEFAULT_REQUEST_TIMEOUT,
) -> bool:
    """
    Register multiple agents in Agentverse using the batch endpoint.

    Args:
        batch_request (BatchRegistrationRequest): The batch registration request containing
            a list of agents to register.
        user_token (str): The user's authentication token for Agentverse.
        agentverse_config (AgentverseConfig | None): The configuration for the agentverse API.
        timeout (int): The timeout for the request.

    Returns:
        bool: True if the batch registration was successful, False otherwise.
    """
    agentverse_config = agentverse_config or AgentverseConfig()
    agents_api = agentverse_config.agents_api
    batch_url = f"{agents_api}/batch"

    logger.debug(
        msg="Registering batch of agents in Agentverse",
        extra={
            "agents_api": agents_api,
            "batch_url": batch_url,
            "agent_count": len(batch_request.agents),
        },
    )

    try:
        _send_post_request_agentverse(
            url=batch_url,
            headers={
                "content-type": "application/json",
                "authorization": f"Bearer {user_token}",
            },
            data=batch_request,
            timeout=timeout,
        )
        logger.info(
            f"Successfully registered batch of {len(batch_request.agents)} agents in Agentverse"
        )
        return True
    except AgentverseRequestError as e:
        logger.error(
            msg=f"Failed to register batch of agents in Agentverse: {str(e)}",
            exc_info=e.from_exc,
        )
        return False
